package com.starview.star.operators.io.runtime;

import com.starview.platform.data.*;
import com.starview.platform.data.type.*;
import com.starview.platform.data.type.Location.NoWhere;
import com.starview.platform.data.value.Factory;
import com.starview.star.compiler.cafe.type.CafeTypeDescription;
import com.starview.star.compiler.util.PrettyPrintDisplay;
import com.starview.star.compiler.util.PrettyPrintable;

import java.io.*;
import java.net.Socket;
import java.util.*;

/**
 * Encapsulate a handle to the file system. May also denote a connection to a remote system -- in which case the file
 * handle will be bi-directional
 * <p/>
 * somewhat equivalent to the type definition:
 * <p/>
 * <pre>
 * type ioHandle is fileReaderHandle{
 *   name has type string;
 *   inputStream has type streamReader;
 * } or fileWriterHandle{
 *   name has type string;
 *   outputStream has type streamWriter;
 * } or socketHandle{
 *   host has type string;
 *   port has type integer;
 *   inputStream has type streamReader;
 *   outputStream has type streamWriter;
 * } or nonFile;
 * </pre>
 *
 * @author fgm
 */
@SuppressWarnings("serial")
public abstract class IOHandle implements PrettyPrintable, IConstructor{
  public static final IType type = TypeUtils.typeExp("ioHandle");

  private static final IType rawStringType = StandardTypes.rawStringType;
  private static final IType rawIntType = StandardTypes.rawIntegerType;
  private static final IType rawBinaryType = StandardTypes.rawBinaryType;

  public static final String NAME = "name";
  public static final String HOST = "host";
  public static final String PORT = "port";
  public static final String INPUT = "inputStream";
  public static final String OUTPUT = "outputStream";

  public static NonFile nonFileEnum = new NonFile();

  public static final TypeInterfaceType face;

  static{
    Map<String, IType> fields = new HashMap<String, IType>();

    fields.put(NAME, rawStringType);
    fields.put(HOST, rawStringType);
    fields.put(PORT, rawIntType);
    fields.put(INPUT, rawBinaryType);
    fields.put(OUTPUT, rawBinaryType);

    face = new TypeInterfaceType(fields);
  }

  public abstract String getName();

  public abstract String getHost();

  public abstract int getPort();

  public abstract BufferedReader getInput();

  public abstract Writer getOutput();

  public abstract void close() throws EvaluationException;

  private boolean eofReached = false;

  public boolean atEof(){
    return eofReached;
  }

  public int readChar() throws EvaluationException{
    if (eofReached)
      throw new EvaluationException("read past end-of-file");
    else if (getInput() == null)
      throw new EvaluationException("not set up for reading");
    else {
      try {
        int reslt = getInput().read();
        if (reslt == -1) {
          eofReached = true;
          throw new EvaluationException("end-of-file");
        }
        return reslt;
      } catch (IOException e) {
        throw new EvaluationException("problem in reading from " + getName(), e);
      }
    }
  }

  public String readLn() throws EvaluationException{
    if (eofReached)
      throw new EvaluationException("read past end-of-file");
    else if (getInput() == null)
      throw new EvaluationException("not set up for reading");
    else {
      try {
        String reslt = getInput().readLine();
        if (reslt == null) {
          eofReached = true;
          throw new EvaluationException("end-of-file");
        }

        return reslt;
      } catch (IOException e) {
        throw new EvaluationException("problem in reading from " + getName(), e);
      }
    }
  }

  public static class ReaderHandle extends IOHandle implements IRecord{
    private static final int conIx = 0;
    private static final String[] members = {NAME, INPUT};
    public static final int nameIndex = 0;
    public static final int inputIndex = 1;
    public static final String label = "fileReaderHandle";
    private static final SortedMap<String, Integer> index = new TreeMap<>();

    static{
      index.put(NAME, nameIndex);
      index.put(INPUT, inputIndex);
    }

    private final String name;
    private final BufferedReader inputStream;

    public ReaderHandle(String name, BufferedReader inputStream){
      this.name = name;
      this.inputStream = inputStream;
    }

    public ReaderHandle(String name) throws EvaluationException{
      try {
        this.name = name;
        this.inputStream = new BufferedReader(new FileReader(name));
      } catch (FileNotFoundException e) {
        throw new EvaluationException("file " + name + " not found", e);
      }
    }

    @Override
    public int conIx(){
      return conIx;
    }

    @Override
    public String getName(){
      return name;
    }

    @Override
    public BufferedReader getInput(){
      return inputStream;
    }

    @Override
    public Writer getOutput(){
      return null;
    }

    @Override
    public String getLabel(){
      return label;
    }

    @Override
    public int size(){
      return 2;
    }

    @Override
    public String getHost(){
      return null;
    }

    @Override
    public int getPort(){
      return 0;
    }

    @Override
    public void close() throws EvaluationException{
      try {
        inputStream.close();
      } catch (IOException e) {
        throw new EvaluationException("problem in closing io handle", e);
      }
    }

    @Override
    public boolean equals(Object obj){
      return obj instanceof ReaderHandle && ((ReaderHandle) obj).name.equals(name)
              && ((ReaderHandle) obj).inputStream == inputStream;
    }

    @Override
    public int hashCode(){
      return (label.hashCode() * 37 + name.hashCode()) * 37 + inputStream.hashCode();
    }

    @Override
    public IRecord copy(){
      return new ReaderHandle(name, inputStream);
    }

    @Override
    public IRecord shallowCopy(){
      return copy();
    }

    @Override
    public void prettyPrint(PrettyPrintDisplay disp){
      disp.append("[");
      disp.append(name);
      disp.append(":");
      disp.append(inputStream.hashCode());
      disp.append("]");
    }

    @Override
    public IValue getCell(int index){
      switch (index) {
        case nameIndex:
          return Factory.newString(name);
        case inputIndex:
          return Factory.newBinary(inputStream);
      }
      throw new IllegalArgumentException("index out of range");
    }

    public String get___0(){
      return name;
    }

    public Reader get___1(){
      return inputStream;
    }

    @Override
    public IValue[] getCells(){
      return new IValue[]{Factory.newString(name), Factory.newBinary(inputStream)};
    }

    @Override
    public IValue getMember(String memberName){
      if (memberName.equals(NAME))
        return Factory.newString(name);
      else if (memberName.equals(INPUT))
        return Factory.newBinary(inputStream);
      else
        return null;
    }

    @Override
    public void setMember(String memberName, IValue value) throws EvaluationException{
      throw new EvaluationException("not permitted");
    }

    @Override
    public String[] getMembers(){
      return members;
    }

    public static IType conType(){
      Map<String, IType> fields = new HashMap<String, IType>();

      fields.put(NAME, rawStringType);
      fields.put(INPUT, rawBinaryType);

      return TypeUtils.constructorType(new TypeInterfaceType(fields), type);
    }
  }

  public static class WriterHandle extends IOHandle implements IRecord{
    private static final int conIx = 1;
    private static final String[] members = {NAME, OUTPUT};
    public static final int nameIndex = 0;
    public static final int outputIndex = 1;
    public static final String label = "fileWriterHandle";
    private static final SortedMap<String, Integer> index = new TreeMap<>();

    static{
      index.put(NAME, nameIndex);
      index.put(INPUT, outputIndex);
    }

    private final String name;
    private final Writer outputStream;

    public WriterHandle(String name, Writer outputStream){
      this.name = name;
      this.outputStream = outputStream;
    }

    public WriterHandle(String name) throws EvaluationException{
      this.name = name;
      try {
        this.outputStream = new FileWriter(name);
      } catch (IOException e) {
        throw new EvaluationException("cannot open " + name + " for writing", e);
      }
    }

    @Override
    public int conIx(){
      return conIx;
    }

    @Override
    public String getName(){
      return name;
    }

    @Override
    public BufferedReader getInput(){
      return null;
    }

    @Override
    public Writer getOutput(){
      return outputStream;
    }

    @Override
    public String getLabel(){
      return label;
    }

    @Override
    public int size(){
      return 2;
    }

    @Override
    public String getHost(){
      return null;
    }

    @Override
    public int getPort(){
      return 0;
    }

    @Override
    public void close() throws EvaluationException{
      try {
        outputStream.close();
      } catch (IOException e) {
        throw new EvaluationException("problem in closing io handle", e);
      }
    }

    @Override
    public boolean equals(Object obj){
      return obj instanceof WriterHandle && ((WriterHandle) obj).name.equals(name)
              && ((WriterHandle) obj).outputStream == outputStream;
    }

    @Override
    public int hashCode(){
      return (label.hashCode() * 37 + name.hashCode()) * 37 + outputStream.hashCode();
    }

    @Override
    public IRecord copy(){
      return new WriterHandle(name, outputStream);
    }

    @Override
    public IRecord shallowCopy(){
      return copy();
    }

    @Override
    public void prettyPrint(PrettyPrintDisplay disp){
      disp.append("[");
      disp.append(name);
      disp.append(":");
      disp.append(outputStream.hashCode());
      disp.append("]");
    }

    @Override
    public IValue getCell(int index){
      switch (index) {
        case nameIndex:
          return Factory.newString(name);
        case outputIndex:
          return Factory.newBinary(outputStream);
      }
      throw new IllegalArgumentException("index out of range");
    }

    public String get___0(){
      return name;
    }

    public Writer get___1(){
      return outputStream;
    }

    @Override
    public IValue[] getCells(){
      return new IValue[]{Factory.newString(name), Factory.newBinary(outputStream)};
    }

    @Override
    public IValue getMember(String memberName){
      if (memberName.equals(NAME))
        return Factory.newString(name);
      else if (memberName.equals(OUTPUT))
        return Factory.newBinary(outputStream);
      else
        return null;
    }

    @Override
    public void setMember(String memberName, IValue value) throws EvaluationException{
      throw new EvaluationException("not permitted");
    }

    @Override
    public String[] getMembers(){
      return members;
    }

    public static IType getConType(){
      Map<String, IType> fields = new HashMap<String, IType>();

      fields.put(NAME, rawStringType);
      fields.put(OUTPUT, rawBinaryType);

      return TypeUtils.constructorType(new TypeInterfaceType(fields), type);
    }
  }

  public static class SocketHandle extends IOHandle implements IRecord{
    private static final int conIx = 2;
    private static final String[] members = {HOST, PORT, INPUT, OUTPUT};
    public static final int hostIndex = 0;
    public static final int portIndex = 1;
    public static final int inputIndex = 2;
    public static final int outputIndex = 3;
    public static final String label = "socketHandle";
    private static final SortedMap<String, Integer> index = new TreeMap<>();

    static {
      index.put(HOST, hostIndex);
      index.put(PORT, portIndex);
      index.put(INPUT, inputIndex);
      index.put(OUTPUT, outputIndex);
    }

    private final String host;
    private final int port;
    private final BufferedReader inputStream;
    private final Writer outputStream;

    public SocketHandle(String host, int port, BufferedReader inputStream, Writer outputStream){
      this.host = host;
      this.port = port;
      this.inputStream = inputStream;
      this.outputStream = outputStream;
    }

    public SocketHandle(String host, int port) throws EvaluationException{
      this.host = host;
      this.port = port;
      try {
        Socket conn = new Socket(host, port);
        try {
          InputStream rdr = conn.getInputStream();
          OutputStream wtr = conn.getOutputStream();
          this.inputStream = new BufferedReader(new InputStreamReader(rdr));
          this.outputStream = new OutputStreamWriter(wtr);
        } finally {
          conn.close();
        }
      } catch (IOException e) {
        throw new EvaluationException("problem in connecting to socket: " + host + ":" + port, e);
      } finally {
      }
    }

    @Override
    public int conIx(){
      return conIx;
    }

    @Override
    public String getName(){
      return null;
    }

    @Override
    public BufferedReader getInput(){
      return inputStream;
    }

    @Override
    public Writer getOutput(){
      return outputStream;
    }

    @Override
    public String getLabel(){
      return label;
    }

    @Override
    public int size(){
      return 4;
    }

    @Override
    public String getHost(){
      return host;
    }

    @Override
    public int getPort(){
      return port;
    }

    @Override
    public void close() throws EvaluationException{
      try {
        inputStream.close();
        outputStream.close();
      } catch (IOException e) {
        throw new EvaluationException("problem in closing io handle", e);
      }
    }

    @Override
    public boolean equals(Object obj){
      if (obj instanceof SocketHandle) {
        SocketHandle socket = (SocketHandle) obj;
        return socket.host.equals(host) && socket.port == port && socket.inputStream == inputStream
                && socket.outputStream == outputStream;
      }
      return false;
    }

    @Override
    public int hashCode(){
      return ((((label.hashCode() * 37 + host.hashCode()) * 37) + port) * 37 + inputStream.hashCode()) * 37
              + outputStream.hashCode();
    }

    @Override
    public IRecord copy(){
      return new SocketHandle(host, port, inputStream, outputStream);
    }

    @Override
    public IRecord shallowCopy(){
      return copy();
    }

    @Override
    public void prettyPrint(PrettyPrintDisplay disp){
      disp.append("[");
      disp.append(host);
      disp.append(":");
      disp.append(port);
      disp.append("]");
    }

    @Override
    public IValue getCell(int index){
      switch (index) {
        case hostIndex:
          return Factory.newString(host);
        case portIndex:
          return Factory.newInt(port);
        case inputIndex:
          return Factory.newBinary(inputStream);
        case outputIndex:
          return Factory.newBinary(outputStream);
      }
      throw new IllegalArgumentException("index out of range");
    }

    public String get___0(){
      return host;
    }

    public int get___1(){
      return port;
    }

    public Reader get___2(){
      return inputStream;
    }

    public Writer get___3(){
      return outputStream;
    }

    @Override
    public IValue[] getCells(){
      return new IValue[]{Factory.newString(host), Factory.newInt(port), Factory.newBinary(inputStream),
              Factory.newBinary(outputStream)};
    }

    @Override
    public IValue getMember(String memberName){
      if (memberName.equals(HOST))
        return Factory.newString(host);
      else if (memberName.equals(PORT))
        return Factory.newInt(getPort());
      else if (memberName.equals(INPUT))
        return Factory.newBinary(inputStream);
      else if (memberName.equals(OUTPUT))
        return Factory.newBinary(outputStream);
      else
        return null;
    }

    @Override
    public void setMember(String memberName, IValue value) throws EvaluationException{
      throw new EvaluationException("not permitted");
    }

    @Override
    public String[] getMembers(){
      return members;
    }

    public static IType getConType(){
      Map<String, IType> fields = new HashMap<String, IType>();

      fields.put(HOST, rawStringType);
      fields.put(PORT, rawIntType);
      fields.put(INPUT, rawBinaryType);
      fields.put(OUTPUT, rawBinaryType);

      return TypeUtils.constructorType(new TypeInterfaceType(fields), type);
    }
  }

  public static class NonFile extends IOHandle{
    public static final String label = "nonFile";
    private static final int conIx = 3;

    @Override
    public void prettyPrint(PrettyPrintDisplay disp){
      disp.append(label);
    }

    @Override
    public int conIx(){
      return conIx;
    }

    @Override
    public String getLabel(){
      return label;
    }

    @Override
    public int size(){
      return 0;
    }

    @Override
    public IValue getCell(int index){
      throw new IllegalAccessError("index out of range");
    }

    @Override
    public IValue[] getCells(){
      return new IValue[]{};
    }

    @Override
    public IConstructor copy() throws EvaluationException{
      return this;
    }

    @Override
    public IConstructor shallowCopy(){
      return this;
    }

    @Override
    public String getName(){
      return null;
    }

    @Override
    public String getHost(){
      return null;
    }

    @Override
    public int getPort(){
      return 0;
    }

    @Override
    public BufferedReader getInput(){
      return null;
    }

    @Override
    public Writer getOutput(){
      return null;
    }

    @Override
    public void close() throws EvaluationException{
    }
  }

  @Override
  public IType getType(){
    return type;
  }

  @Override
  public void accept(IValueVisitor visitor){
    visitor.visitConstructor(this);
  }

  @Override
  public String toString(){
    return PrettyPrintDisplay.toString(this);
  }

  @Override
  public void setCell(int index, IValue value) throws EvaluationException{
    throw new IllegalArgumentException("not permitted");
  }

  public static void declare(ITypeContext cxt){
    NoWhere nullLoc = Location.noWhereEnum;
    ConstructorSpecifier rdrSpec = new RecordSpecifier(nullLoc, ReaderHandle.label, ReaderHandle.conIx, null,
            ReaderHandle.conType(), ReaderHandle.class, IOHandle.class);
    ConstructorSpecifier wtrSpec = new RecordSpecifier(nullLoc, WriterHandle.label, WriterHandle.conIx, null,
            WriterHandle.getConType(), WriterHandle.class, IOHandle.class);
    ConstructorSpecifier sockSpec = new RecordSpecifier(nullLoc, SocketHandle.label, SocketHandle.conIx, null,
            SocketHandle.getConType(), SocketHandle.class, IOHandle.class);

    ConstructorSpecifier nonFileSpec = new ConstructorSpecifier(nullLoc, null, NonFile.label, NonFile.conIx, TypeUtils
            .constructorType(type), NonFile.class, IOHandle.class);

    List<IValueSpecifier> specs = new ArrayList<IValueSpecifier>();
    specs.add(rdrSpec);
    specs.add(wtrSpec);
    specs.add(sockSpec);
    specs.add(nonFileSpec);

    ITypeDescription ioHandleDesc = new CafeTypeDescription(nullLoc, type, IOHandle.class.getName(), specs);

    cxt.defineType(ioHandleDesc);
  }
}
